package de.pa2.proxy;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.zip.GZIPInputStream;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpClientConnection;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.protocol.HttpRequestHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.pa2.proxy.config.HostConfiguration;
import de.pa2.proxy.config.Replacement;
import de.pa2.proxy.config.ServerConfiguration;

public class ProxyHandler implements HttpRequestHandler {
	private static Logger LOG = LoggerFactory.getLogger(ProxyHandler.class);
	private final HttpProcessor httpproc;
	private final HttpRequestExecutor httpexecutor;
	private final ConnectionReuseStrategy connStrategy;
	private final HttpParams params;
	private final ServerConfiguration configuration;

	public ProxyHandler(final HttpProcessor httpproc,
			final HttpRequestExecutor httpexecutor,
			final ServerConfiguration configuration) {
		super();
		this.configuration = configuration;
		this.httpproc = httpproc;
		this.httpexecutor = httpexecutor;
		this.connStrategy = new DefaultConnectionReuseStrategy();
		this.params = new SyncBasicHttpParams();
		this.params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
				.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE,
						8 * 1024).setBooleanParameter(
						CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
				.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
				.setParameter(CoreProtocolPNames.ORIGIN_SERVER,
						"HttpComponents/1.1");
	}

	public void handle(final HttpRequest request, final HttpResponse response,
			final HttpContext context) throws HttpException, IOException {
		HttpHost target = null;
		String url = request.getRequestLine().getUri();
		LOG.debug("handle url='" + url + "'");
		Url u = UrlUtil.extract(url);
		String host = u.getHostname();
		int port = u.getPort();

		if (LOG.isDebugEnabled()) {
			LOG.debug("handling request to '" + url + "'");
		}

		// there is a reason the proxy is used. find the reason and act as
		// instructed
		HostConfiguration current = null;
		for (HostConfiguration cfg : configuration.getHostConfigurations()) {
			for (String domain : cfg.getDomains()) {
				if (MiscUtils.hostnameMatches(domain, host)) {
					// use this configuration
					current = cfg;
					break;
				}
			}
		}

		if (current == null) {
			LOG.warn("handling request to host '" + host
					+ "' but did not find a fitting configuration for url '"
					+ u.toString());

		}

		Socket outsocket = new Socket(host, port);
		DefaultHttpClientConnection conn = new DefaultHttpClientConnection();
		conn.bind(outsocket, this.params);
		context.setAttribute(Constants.HTTP_OUT_CONN, conn);
		context.setAttribute(ExecutionContext.HTTP_CONNECTION, conn);
		context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, target);

		// Remove hop-by-hop headers
		request.removeHeaders(HTTP.CONTENT_LEN);
		request.removeHeaders(HTTP.TRANSFER_ENCODING);
		request.removeHeaders(HTTP.CONN_DIRECTIVE);
		request.removeHeaders("Keep-Alive");
		request.removeHeaders("Proxy-Authenticate");
		request.removeHeaders("TE");
		request.removeHeaders("Trailers");
		request.removeHeaders("Upgrade");

		// check cookies
		if (current != null && current.isAcceptCookies()) {
			if (LOG.isDebugEnabled()) {
				Header[] cookies = request.getHeaders("Set-Cookie");
				if (cookies != null && cookies.length > 0) {
					for (Header cookie : cookies) {
						LOG.debug("using cookie:" + cookie.getValue());
					}
				}
			}
		} else {
			if (LOG.isDebugEnabled()) {
				Header[] cookies = request.getHeaders("Cookie");
				if (cookies != null && cookies.length > 0) {
					for (Header cookie : cookies) {
						LOG.debug("hiding cookie:" + cookie.getValue());
					}
				}
			}
			request.removeHeaders("Cookie");
		}

		if (LOG.isDebugEnabled()) {
			Header[] headers = request.getAllHeaders();
			if (headers != null && headers.length > 0) {
				for (Header h : headers) {
					LOG.debug("using header: " + h.getName() + "="
							+ h.getValue());
				}
			}
		}

		this.httpexecutor.preProcess(request, this.httpproc, context);
		HttpResponse targetResponse = this.httpexecutor.execute(request, conn,
				context);
		this.httpexecutor.postProcess(response, this.httpproc, context);

		// Remove hop-by-hop headers
		targetResponse.removeHeaders(HTTP.CONTENT_LEN);
		targetResponse.removeHeaders(HTTP.TRANSFER_ENCODING);
		targetResponse.removeHeaders(HTTP.CONN_DIRECTIVE);
		targetResponse.removeHeaders("Keep-Alive");
		targetResponse.removeHeaders("TE");
		targetResponse.removeHeaders("Trailers");
		targetResponse.removeHeaders("Upgrade");

		response.setStatusLine(targetResponse.getStatusLine());
		response.setHeaders(targetResponse.getAllHeaders());

		HttpEntity entity = targetResponse.getEntity();
		if (entity != null && entity.getContentType() != null) {

			String contentType = entity.getContentType().getValue();
			String charset = "utf8";
			// Charset cs = Charset.defaultCharset();
			if (contentType.contains(";")) {
				charset = contentType.split(";")[1].split("=")[1].trim();
			}
			if (current != null) {
				if (contentType.startsWith("text/html")) {
					ByteArrayOutputStream tmp = new ByteArrayOutputStream();
					entity.writeTo(tmp);
					byte[] responseBytes = tmp.toByteArray();

					boolean gzippedStream = entity.getContentEncoding() != null
							&& "gzip".equals(entity.getContentEncoding()
									.getValue());
					InputStream in = null;
					if (gzippedStream) {
						// read gziped
						in = new GZIPInputStream(new ByteArrayInputStream(
								responseBytes));
						response.removeHeaders(HTTP.CONTENT_ENCODING);
					} else {
						// read plain
						in = new ByteArrayInputStream(responseBytes);
					}

					final ByteArrayOutputStream decoded = new ByteArrayOutputStream();
					final byte buffer[] = new byte[1024];

					for (int length; (length = in.read(buffer, 0, 1024)) != -1;) {
						decoded.write(buffer, 0, length);
					}

					// closing
					in.close();
					decoded.close();
					String body = decoded.toString(charset);
					for (Replacement r : current.getReplacements()) {
						if (r.getPattern() != null) {
							// use regular expression
							body = body.replaceAll(r.getPattern(), r
									.getReplacement());
						} else if (r.getLookup() != null) {
							// use string replace
							body = body.replace(r.getLookup(), r
									.getReplacement());
						} else {
							LOG
									.error("no pattern and no lookup set for replacement:"
											+ r.toString());
						}
					}
					entity = new StringEntity(body, charset);
				} else {
					// non (x)html
					LOG.debug("handling contentType='"+contentType+"'");
				}

				// check cookies
				if (current.isAcceptCookies()) {
					if (LOG.isDebugEnabled()) {
						Header[] cookies = response.getHeaders("Set-Cookie");
						if (cookies != null && cookies.length > 0) {
							for (Header cookie : cookies) {
								LOG.debug("accepting and forwarding cookie:"
										+ cookie.getValue());
							}
						}
					}
				} else {
					if (LOG.isDebugEnabled()) {
						Header[] cookies = response.getHeaders("Set-Cookie");
						if (cookies != null && cookies.length > 0) {
							for (Header cookie : cookies) {
								LOG.debug("removing cookie:"
										+ cookie.getValue());
							}
						}
					}
					response.removeHeaders("Set-Cookie");
				}
			}
			response.setHeader("Content-Type", contentType);
			response.setEntity(entity);
		}

		LOG.debug("response for '" + url + "' : " + response.getStatusLine());

		boolean keepalive = this.connStrategy.keepAlive(response, context);
		context.setAttribute(Constants.HTTP_CONN_KEEPALIVE, new Boolean(
				keepalive));
	}
}